package com.gitee.wsl.time

import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.microseconds
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
import kotlin.time.ExperimentalTime

class RoundDownTest {
    private val testOffset = (-5).hours.offset
    private val nyZone = TimeZone.of("America/New_York")

    @Test
    fun `round down to nearest 2-hours`() {
        testRoundedDownToNearest(
            2.hours,
            "1970-01-01T00:00" to "1970-01-01T00:00",
            "1969-12-31T01:00" to "1969-12-31T00:00",
            "1969-12-31T00:59:59.999999999" to "1969-12-31T00:00",
            "1969-12-31T00:00:00.000000001" to "1969-12-31T00:00",
            "1969-12-30T23:59:59.999999999" to "1969-12-30T22:00"
        )
    }


    @Test
    fun `round down to nearest 2-minutes`() {
        testRoundedDownToNearest(
            2.minutes,
            "1970-01-01T00:00" to "1970-01-01T00:00",
            "1969-12-31T01:59" to "1969-12-31T01:58",
            "1969-12-31T02:01:01.9" to "1969-12-31T02:00",
            "1970-01-01T00:10:59" to "1970-01-01T00:10",
            "1970-01-01T00:01:59.999999999" to "1970-01-01T00:00"
        )
    }

    @Test
    fun `round down to nearest 30-minutes`() {
        testRoundedDownToNearest(
            30.minutes,
            "1970-01-01T00:00" to "1970-01-01T00:00",
            "1969-12-31T01:15:00.000000001" to "1969-12-31T01:00",
            "1969-12-31T01:29:59.999" to "1969-12-31T01:00",
            "1970-01-01T00:15:30" to "1970-01-01T00:00"
        )
    }

    @Test
    fun `round down to nearest 2-seconds`() {
        testRoundedDownToNearest(
            2.seconds,
            "1970-01-01T00:00" to "1970-01-01T00:00",
            "1969-12-31T01:59:01" to "1969-12-31T01:59:00",
            "1969-12-31T02:01:01.9" to "1969-12-31T02:01:00",
            "1970-01-01T00:01:59" to "1970-01-01T00:01:58"
        )
    }


    @Test
    fun `round down to nearest 2-milliseconds`() {
        testRoundedDownToNearest(
            2.milliseconds,
            "1970-01-01T00:00" to "1970-01-01T00:00",
            "1969-12-31T01:59:01.999" to "1969-12-31T01:59:01.998",
            "1969-12-31T02:00:01.001" to "1969-12-31T02:00:01",
            "1970-01-01T00:01:59.003" to "1970-01-01T00:01:59.002"
        )
    }

    @Test
    fun `round down to nearest 100-milliseconds`() {
        testRoundedDownToNearest(
            100.milliseconds,
            "1970-01-01T00:00" to "1970-01-01T00:00",
            "1969-12-31T01:59:01.95" to "1969-12-31T01:59:01.9",
            "1969-12-31T02:00:01.001" to "1969-12-31T02:00:01",
            "1970-01-01T00:01:59.01" to "1970-01-01T00:01:59"
        )
    }

    @Test
    fun `round down to nearest 1000-milliseconds`() {
        testRoundedDownToNearest(
            1_000.milliseconds,
            "1970-01-01T00:00" to "1970-01-01T00:00",
            "1969-12-31T01:59:01.95" to "1969-12-31T01:59:01",
            "1969-12-31T01:59:01.949999999" to "1969-12-31T01:59:01",
            "1970-01-01T00:01:59.01" to "1970-01-01T00:01:59"
        )
    }


    @Test
    fun `round down to nearest 100-microseconds`() {
        testRoundedDownToNearest(
            100.microseconds,
            "1970-01-01T00:00" to "1970-01-01T00:00",
            "1969-12-31T01:59:01.99995" to "1969-12-31T01:59:01.9999",
            "1970-01-01T00:01:59.00001" to "1970-01-01T00:01:59"
        )
    }


    @Test
    fun `round down to nearest 2-nanoseconds`() {
        testRoundedDownToNearest(
            2.nanoseconds,
            "1970-01-01T00:00" to "1970-01-01T00:00",
            "1969-12-31T01:59:01.999999999" to "1969-12-31T01:59:01.999999998",
            "1969-12-31T01:59:01.999999998" to "1969-12-31T01:59:01.999999998"
        )
    }

    @Test
    fun `round down to days`() {
        testRoundedDownTo(
            DurationUnit.DAYS,
            "1970-01-01T00:00" to "1970-01-01T00:00",
            "1969-12-31T23:59:59.999999999" to "1969-12-31T00:00"
        )
    }

    @Test
    fun `round down to hours`() {
        testRoundedDownTo(
            DurationUnit.HOURS,
            "1970-01-01T00:00" to "1970-01-01T00:00",
            "1969-12-31T23:59:59.999999999" to "1969-12-31T23:00"
        )
    }

    @Test
    fun `round down to minutes`() {
        testRoundedDownTo(
            DurationUnit.MINUTES,
            "1970-01-01T00:00" to "1970-01-01T00:00",
            "1969-12-31T23:59:59.999999999" to "1969-12-31T23:59"
        )
    }

    @Test
    fun `round down to seconds`() {
        testRoundedDownTo(
            DurationUnit.SECONDS,
            "1970-01-01T00:00" to "1970-01-01T00:00",
            "1969-12-31T23:59:59.999999999" to "1969-12-31T23:59:59"
        )
    }

    @Test
    fun `round down to milliseconds`() {
        testRoundedDownTo(
            DurationUnit.MILLISECONDS,
            "1970-01-01T00:00" to "1970-01-01T00:00",
            "1969-12-31T23:59:59.999999999" to "1969-12-31T23:59:59.999"
        )
    }

    @Test
    fun `round down to microseconds`() {
        testRoundedDownTo(
            DurationUnit.MICROSECONDS,
            "1970-01-01T00:00" to "1970-01-01T00:00",
            "1969-12-31T23:59:59.999999999" to "1969-12-31T23:59:59.999999"
        )
    }

    @Test
    fun `rounding to nanoseconds does nothing`() {
        testRoundedDownTo(
            DurationUnit.NANOSECONDS,
            "1970-01-01T00:00" to "1970-01-01T00:00",
            "1969-12-31T23:59:59.999999999" to "1969-12-31T23:59:59.999999999"
        )
    }


//    private val testObjects = listOf(
//        Time.MIDNIGHT,
//        Time.MIDNIGHT.at(testOffset),
//        DateTime(2012, 5, 5, 0, 0),
//        DateTime(2012, 5, 5, 0, 0).at(testOffset),
//        DateTime(2012, 5, 5, 0, 0).at(nyZone),
//        Instant.UNIX_EPOCH
//    )
//
//    private val testOperations = listOf<Any.(IntHours) -> Any>(
//        Time::roundedDownToNearest,
//        OffsetTime::roundedDownToNearest,
//        DateTime::roundedDownToNearest,
//        OffsetDateTime::roundedDownToNearest,
//        ZonedDateTime::roundedDownToNearest,
//        Instant::roundedDownToNearest
//    )
//
//    private inline fun <T, U> testException(objs: List<T>, quantities: List<U>, operation: T.(U) -> T) {
//        for (obj in objs) {
//            for (quantity in quantities) {
//                assertFailsWith<IllegalArgumentException>("Failed at '$obj'") {
//                    obj.operation(quantity)
//                }
//            }
//        }
//    }

    @OptIn(ExperimentalTime::class)
    private fun testRoundedDownTo(unit: DurationUnit, vararg dateTimeValues: Pair<String, String>) {
        dateTimeValues.map { (input, output) -> LocalDateTime.parse(input) to LocalDateTime.parse(output) }
            .forEach { (inDateTime, outDateTime) ->
                assertEquals(outDateTime, inDateTime.roundedDownTo(unit))
                assertEquals(outDateTime at testOffset, inDateTime.at(testOffset).roundedDownTo(unit))
                assertEquals(outDateTime at nyZone, inDateTime.at(nyZone).roundedDownTo(unit))
                assertEquals(outDateTime.time, inDateTime.time.roundedDownTo(unit))
                assertEquals(outDateTime.time at testOffset, inDateTime.time.at(testOffset).roundedDownTo(unit))

            }
    }


    private fun testRoundedDownToNearest(increment: Duration, vararg dateTimeValues: Pair<String, String>) {
        dateTimeValues.map { (input, output) -> LocalDateTime.parse(input) to LocalDateTime.parse(output) }
            .forEach { (inDateTime, outDateTime) ->
                assertEquals(outDateTime, inDateTime.roundedDownToNearest(increment))
                assertEquals(outDateTime at testOffset, inDateTime.at(testOffset).roundedDownToNearest(increment))
                assertEquals(outDateTime at nyZone, inDateTime.at(nyZone).roundedDownToNearest(increment))
                assertEquals(outDateTime.time, inDateTime.time.roundedDownToNearest(increment))

                assertEquals(
                    outDateTime.time at testOffset,
                    inDateTime.time.at(testOffset).roundedDownToNearest(increment)
                )

            }
    }

}
